// Written in the D Programming Language
/**
 * The 22nd lesson in the <a href="http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=22">NeHe tutorial series</a>.
 * Originally written by Jens Schneider and Jeff Molofee.
 *
 * Authors: Jens Schneider
 * 			Jeff Molofee
 *          Olli Aalto
 */
module lesson22;

import derelict.opengl.gl;
import derelict.opengl.glu;
import derelict.opengl.extension.arb.multitexture;
import derelict.opengl.extension.ext.texture_env_combine;
import derelict.sdl.sdl;

import tango.stdc.stringz;
import Math = tango.math.Math;

/// The window title
const char[]
	WINDOW_TITLE = "NeHe's GL_ARB_multitexture & Bump Mapping Tutorial (D version)";

/// The main loop flag
bool running;

const MAX_EMBOSS = 0.008f; // Maximum Emboss-Translate. Increase To Get Higher Immersion
// At A Cost Of Lower Quality (More Artifacts Will Occur!)

bool multitextureSupported; // Flag Indicating Whether Multitexturing Is Supported
bool useMultitexture = true; // Use It If It Is Supported?
GLint maxTexelUnits = 1; // Number Of Texel-Pipelines. This Is At Least 1.

bool emboss; // Emboss Only, No Basetexture?
bool bumps = true; // Do Bumpmapping?

/// X Rotation
GLfloat xrot = 0.0f;
/// Y Rotation
GLfloat yrot = 0.0f;
/// Z Rotation
GLfloat zrot = 0.0f;
/// X Rotation Speed
GLfloat xspeed = 0.0f;
/// Y Rotation Speed
GLfloat yspeed = 0.0f;
/// Depth Into The Screen
GLfloat z = -5.0f;

GLuint filter; // Which Filter To Use
GLuint[3] texture; // Storage For 3 Textures
GLuint[3] bump; // Our Bumpmappings
GLuint[3] invbump; // Inverted Bumpmaps
GLuint glLogo; // Handle For OpenGL-Logo
GLuint multiLogo; // Handle For Multitexture-Enabled-Logo

GLfloat[] lightAmbient = [0.2f, 0.2f, 0.2f]; // Ambient Light is 20% white
GLfloat[] lightDiffuse = [1.0f, 1.0f, 1.0f]; // Diffuse Light is white
GLfloat[] lightPosition = [0.0f, 0.0f, 2.0f]; // Position is somewhat in front of screen

GLfloat[] grayColor = [0.5f, 0.5f, 0.5f, 1.0f];

// Data Contains The Faces For The Cube In Format 2xTexCoord, 3xVertex;
// Note That The Tesselation Of The Cube Is Only Absolute Minimum.
GLfloat[] data = [
	// FRONT FACE
	0.0f, 0.0f, -1.0f, -1.0f, +1.0f, 1.0f, 0.0f, +1.0f, -1.0f, +1.0f, 1.0f,
	1.0f, +1.0f, +1.0f, +1.0f, 0.0f,
	1.0f,
	-1.0f,
	+1.0f,
	+1.0f,
	// BACK FACE
	1.0f, 0.0f, -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, +1.0f, -1.0f, 0.0f,
	1.0f, +1.0f, +1.0f, -1.0f, 0.0f, 0.0f,
	+1.0f,
	-1.0f,
	-1.0f,
	// Top Face
	0.0f, 1.0f, -1.0f, +1.0f, -1.0f, 0.0f, 0.0f, -1.0f, +1.0f, +1.0f, 1.0f,
	0.0f, +1.0f, +1.0f, +1.0f, 1.0f, 1.0f, +1.0f,
	+1.0f,
	-1.0f,
	// Bottom Face
	1.0f, 1.0f, -1.0f, -1.0f, -1.0f, 0.0f, 1.0f, +1.0f, -1.0f, -1.0f, 0.0f,
	0.0f, +1.0f, -1.0f, +1.0f, 1.0f, 0.0f, -1.0f, -1.0f,
	+1.0f,
	// Right Face
	1.0f, 0.0f, +1.0f, -1.0f, -1.0f, 1.0f, 1.0f, +1.0f, +1.0f, -1.0f, 0.0f,
	1.0f, +1.0f, +1.0f, +1.0f, 0.0f, 0.0f, +1.0f, -1.0f, +1.0f,
	// Left Face
	0.0f, 0.0f, -1.0f, -1.0f, -1.0f, 1.0f, 0.0f, -1.0f, -1.0f, +1.0f, 1.0f,
	1.0f, -1.0f, +1.0f, +1.0f, 0.0f, 1.0f, -1.0f, +1.0f, -1.0f];

/**
 * Module constructor. Here we load the GL, GLU and SDL shared libraries,
 * and the initialize SDL.
 */
static this()
{
	DerelictGL.load();
	DerelictGLU.load();
	DerelictSDL.load();

	if (SDL_Init(SDL_INIT_VIDEO) < 0)
	{
		throw new Exception("Failed to initialize SDL: " ~ getSDLError());
	}
	// Enable key repeating
	if ((SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY,
		SDL_DEFAULT_REPEAT_INTERVAL)))
	{
		throw new Exception("Failed to set key repeat: " ~ getSDLError());
	}
}

/**
 * Module destructor. SDL_Quit must be called somewhere, and as we initialized
 * it in the module constructor so the module destructor should be a suitable
 * place.
 */
static ~this()
{
	SDL_Quit();
}

/**
 * The main function. This is where the fun begins. The first order of business
 * is the check the command line arguments if the user wanted to start in
 * fullscreen mode. Then the window is created and OpenGL is initialized with
 * basic settings. Finally the the function starts the main loop which will live
 * for the duration of the application.
 *
 * Params:
 *      args = the command line arguments
 */
void main(char[][] args)
{
	bool fullScreen = false;
	if (args.length > 1)
	{
		fullScreen = args[1] == "-fullscreen";
	}

	createGLWindow(WINDOW_TITLE, 640, 480, 16, fullScreen);
	initGL();

	running = true;
	while (running)
	{
		processEvents();

		drawGLScene();

		SDL_GL_SwapBuffers();
		SDL_Delay(10);
	}
}

void loadExtensions()
{
	DerelictGL.loadExtensions();
	if (!ARBMultitexture.isEnabled())
	{
		throw new Exception("Failed to load ARB MultiTexture extension!");
	}
	if (!EXTTextureEnvCombine.isEnabled())
	{
		throw new Exception("Failed to load EXT Texture Env Combine extension!");
	}
	glGetIntegerv(GL_MAX_TEXTURE_UNITS_ARB, &maxTexelUnits);
	multitextureSupported = true;
}

void createTextures(GLuint[] textureIds, int width, int height, void* data)
{
	// Create The Texture
	glGenTextures(textureIds.length, &textureIds[0]);

	// Create Nearest Filtered Texture
	glBindTexture(GL_TEXTURE_2D, textureIds[0]);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, width, height, 0, GL_BGR,
		GL_UNSIGNED_BYTE, data);

	glBindTexture(GL_TEXTURE_2D, textureIds[1]);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, width, height, 0, GL_BGR,
		GL_UNSIGNED_BYTE, data);

	glBindTexture(GL_TEXTURE_2D, textureIds[2]);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
		GL_LINEAR_MIPMAP_NEAREST);
	gluBuild2DMipmaps(GL_TEXTURE_2D, GL_RGB8, width, height, GL_BGR,
		GL_UNSIGNED_BYTE, data);
}

void createTextures(GLuint[] textureIds, SDL_Surface* textureImage)
{
	createTextures(textureIds, textureImage.w, textureImage.h,
		textureImage.pixels);
}

void loadBaseTexture()
{
	SDL_Surface* textureImage;

	// Load The Bitmap, Check For Errors, If Bitmap's Not Found Quit
	if ((textureImage = SDL_LoadBMP("data/Base.bmp")) !is null)
	{
		scope(exit)
			SDL_FreeSurface(textureImage);

		createTextures(texture, textureImage);

		return;
	}
	throw new Exception("Failed to load texture Base.bmp");
}

void loadBumpmapTexture()
{
	SDL_Surface* textureImage;

	// Load The Bumpmaps
	if ((textureImage = SDL_LoadBMP("data/Bump.bmp")) !is null)
	{
		scope(exit)
			SDL_FreeSurface(textureImage);

		glPixelTransferf(GL_RED_SCALE, 0.5f); // Scale RGB By 50%, So That We Have Only			
		glPixelTransferf(GL_GREEN_SCALE, 0.5f); // Half Intenstity
		glPixelTransferf(GL_BLUE_SCALE, 0.5f);

		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); // No Wrapping, Please!
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
		glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, grayColor.ptr);

		createTextures(bump, textureImage);

		byte* buf = cast(byte*) textureImage.pixels;
		for (int i = 0; i < 3 * textureImage.w * textureImage.h; i++)
		// Invert The Bumpmap
		{
			buf[i] = 255 - buf[i];
		}

		createTextures(invbump, textureImage.w, textureImage.h, buf);

		glPixelTransferf(GL_RED_SCALE, 1.0f); // Scale RGB Back To 100% Again		
		glPixelTransferf(GL_GREEN_SCALE, 1.0f);
		glPixelTransferf(GL_BLUE_SCALE, 1.0f);

		return;
	}
	throw new Exception("Failed to load texture Bump.bmp");
}

void loadAlphaTexture(char[] file1, char[] file2, inout GLuint textureId)
{
	SDL_Surface* textureImage;

	// Load The "Extension Enabled"-Logo
	if ((textureImage = SDL_LoadBMP(toStringz(file1))) !is null)
	{
		byte[] alpha = getAlpha(textureImage);

		SDL_FreeSurface(textureImage);
		if ((textureImage = SDL_LoadBMP(toStringz(file2))) !is null)
		{
			scope(exit)
				SDL_FreeSurface(textureImage);

			createLogoTexture(alpha, textureImage, textureId);
		}
		else
		{
			throw new Exception("Failed to load texture " ~ file2);
		}
		return;
	}
	throw new Exception("Failed to load texture " ~ file1);
}

/**
 * Load the texture used in this tutorial.
 */
void loadGLTextures()
{
	loadBaseTexture();
	loadBumpmapTexture();
	loadAlphaTexture("data/OpenGL_Alpha.bmp", "data/OpenGL.bmp", glLogo);
	loadAlphaTexture("data/Multi_On_Alpha.bmp", "data/Multi_On.bmp", multiLogo);
}

void createLogoTexture(byte[] alpha, SDL_Surface* textureImage,
	inout GLuint texture)
{
	byte* tmpData = cast(byte*) textureImage.pixels;
	for (int a = 0; a < textureImage.w * textureImage.h; a++)
	{
		alpha[4 * a] = tmpData[a * 3]; // R
		alpha[4 * a + 1] = tmpData[a * 3 + 1]; // G
		alpha[4 * a + 2] = tmpData[a * 3 + 2]; // B
	}

	glGenTextures(1, &texture); // Create One Textures

	// Create Linear Filtered RGBA8-Texture
	glBindTexture(GL_TEXTURE_2D, texture);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, textureImage.w, textureImage.h, 0,
		GL_BGRA, GL_UNSIGNED_BYTE, alpha.ptr);
}

byte[] getAlpha(SDL_Surface* textureImage)
{
	byte* tmpData = cast(byte*) textureImage.pixels;
	byte[] alpha = new byte[4 * textureImage.w * textureImage.h]; // Create Memory For RGBA8-Texture
	for (int a = 0; a < textureImage.w * textureImage.h; a++)
	{
		alpha[4 * a + 3] = tmpData[a * 3]; // Pick Only Red Value As Alpha!
	}
	return alpha;
}

void initLights()
{
	// Load Light-Parameters Into GL_LIGHT1
	glLightfv(GL_LIGHT1, GL_AMBIENT, lightAmbient.ptr);
	glLightfv(GL_LIGHT1, GL_DIFFUSE, lightDiffuse.ptr);
	glLightfv(GL_LIGHT1, GL_POSITION, lightPosition.ptr);

	glEnable(GL_LIGHT1);
}

/**
 * Process all the pending events.
 */
void processEvents()
{
	SDL_Event event;
	while (SDL_PollEvent(&event))
	{
		switch (event.type)
		{
			case SDL_KEYUP:
				keyReleased(event.key.keysym.sym);
				break;
			case SDL_KEYDOWN:
				keyPressed(event.key.keysym.sym);
				break;
			case SDL_QUIT:
				running = false;
				break;
			default:
				break;
		}
	}
}

void keyPressed(int key)
{
	switch (key)
	{
		case SDLK_PAGEUP:
			// PageUp key was pressed this zooms into the screen
			z -= 0.02f;
			break;
		case SDLK_PAGEDOWN:
			// PageDown key was pressed this zooms out of the screen
			z += 0.02f;
			break;
		case SDLK_UP:
			// Up arrow key was pressed this increases the x rotation speed
			xspeed -= 0.01f;
			break;
		case SDLK_DOWN:
			// Down arrow key was pressed this decreases the x rotation speed
			xspeed += 0.01f;
			break;
		case SDLK_RIGHT:
			// Right arrow key was pressed this increases the y rotation speed
			yspeed += 0.01f;
			break;
		case SDLK_LEFT:
			// Left arrow key was pressed this decreases the y rotation speed
			yspeed -= 0.01f;
			break;
		default:
			break;
	}
}

/**
 * Process a key released event.
 */
void keyReleased(int key)
{
	switch (key)
	{
		case SDLK_ESCAPE:
			running = false;
			break;
		case SDLK_e:
			// 'e' key was pressed this toggles embossing
			emboss = !emboss;
			break;
		case SDLK_m:
			// 'm' key was pressed this toggles multitextured support
			useMultitexture = !useMultitexture && multitextureSupported;
			break;
		case SDLK_b:
			// 'b' key was pressed this toggles bumps 
			bumps = !bumps;
			break;
		case SDLK_f:
			// 'f' key was pressed this pages through the different filters
			filter = (++filter) % 3;
			break;
		default:
			break;
	}
}

/**
 * Resize and initialize the OpenGL window.
 */
void resizeGLScene(GLsizei width, GLsizei height)
{
	if (height == 0)
	{
		height = 1;
	}
	// Reset The Current Viewport
	glViewport(0, 0, width, height);

	// Select The Projection Matrix
	glMatrixMode(GL_PROJECTION);

	// Reset The Projection Matrix
	glLoadIdentity();

	// Calculate The Aspect Ratio Of The Window
	gluPerspective(45.0f, cast(GLfloat) width / cast(GLfloat) height, 0.1f,
		100.0f);

	// Select The Modelview Matrix
	glMatrixMode(GL_MODELVIEW);

	// Reset The Modelview Matrix
	glLoadIdentity();
}

/**
 * Initialize OpenGL.
 */
void initGL()
{
	// Load needed extensions
	loadExtensions();
	// Load the texture
	loadGLTextures();
	// Enable Texture Mapping
	glEnable(GL_TEXTURE_2D);
	// Enables Smooth Shading
	glShadeModel(GL_SMOOTH);
	// Black Background
	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
	// Depth Buffer Setup
	glClearDepth(1.0f);
	// Enables Depth Testing
	glEnable(GL_DEPTH_TEST);
	// The Type Of Depth Test To Do
	glDepthFunc(GL_LEQUAL);
	// Really Nice Perspective Calculations
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
	// Change to texture matrix and flip and rotate the texture
	glMatrixMode(GL_TEXTURE);
	glRotatef(180.0f, 0.0f, 0.0f, 1.0f);
	glScalef(-1.0f, 1.0f, 1.0f);
	// Back to normal
	glMatrixMode(GL_MODELVIEW);
	// Initialize OpenGL Light
	initLights();
}

/*	Okay, Here Comes The Important Stuff:

 On http://www.nvidia.com/marketing/Developer/DevRel.nsf/TechnicalDemosFrame?OpenPage
 You Can Find A Demo Called GL_BUMP That Is A Little Bit More Complicated.
 GL_BUMP:   Copyright Diego Tartara, 1999.			
 -  diego_tartara@ciudad.com.ar  -

 The Idea Behind GL_BUMP Is, That You Compute The Texture-Coordinate Offset As Follows:
 0) All Coordinates Either In Object Or In World Space.
 1) Calculate Vertex v From Actual Position (The Vertex You're At) To The Lightposition
 2) Normalize v
 3) Project This v Into Tangent Space.
 Tangent Space Is The Plane "Touching" The Object In Our Current Position On It.
 Typically, If You're Working With Flat Surfaces, This Is The Surface Itself.
 4) Offset s,t-Texture-Coordinates By The Projected v's x And y-Component.

 * This Would Be Called Once Per Vertex In Our Geometry, If Done Correctly.
 * This Might Lead To Incoherencies In Our Texture Coordinates, But Is Ok As Long As You Did Not
 * Wrap The Bumpmap.
 
 Basically, We Do It The Same Way With Some Exceptions:
 ad 0) We'll Work In Object Space All Time. This Has The Advantage That We'll Only
 Have To Transform The Lightposition From Frame To Frame. This Position Obviously
 Has To Be Transformed Using The Inversion Of The Modelview Matrix. This Is, However,
 A Considerable Drawback, If You Don't Know How Your Modelview Matrix Was Built, Since
 Inverting A Matrix Is Costly And Complicated.
 ad 1) Do It Exactly That Way.
 ad 2) Do It Exactly That Way.
 ad 3) To Project The Lightvector Into Tangent Space, We'll Support The Setup-Routine
 With Two Directions: One Of Increasing s-Texture-Coordinate Axis, The Other In
 Increasing t-Texture-Coordinate Axis. The Projection Simply Is (Assumed Both
 texCoord Vectors And The Lightvector Are Normalized) The Dotproduct Between The
 Respective texCoord Vector And The Lightvector. 
 ad 4) The Offset Is Computed By Taking The Result Of Step 3 And Multiplying The Two
 Numbers With MAX_EMBOSS, A Constant That Specifies How Much Quality We're Willing To
 Trade For Stronger Bump-Effects. Just Temper A Little Bit With MAX_EMBOSS!

 WHY THIS IS COOL:
 * Have A Look!
 * Very Cheap To Implement (About One Squareroot And A Couple Of MULs)!
 * Can Even Be Further Optimized!
 * SetUpBump Doesn't Disturb glBegin()/glEnd()
 * THIS DOES ALWAYS WORK - Not Only With XY-Tangent Spaces!!

 DRAWBACKS:
 * Must Know "Structure" Of Modelview-Matrix Or Invert It. Possible To Do The Whole Thing
 * In World Space, But This Involves One Transformation For Each Vertex!
 */

void setUpBumps(GLfloat[] n, GLfloat[] c, GLfloat[] l, GLfloat[] s, GLfloat[] t)
{
	GLfloat[3] v; // Vertex From Current Position To Light	

	// Calculate v From Current Vector c To Lightposition And Normalize v	
	v[0] = l[0] - c[0];
	v[1] = l[1] - c[1];
	v[2] = l[2] - c[2];
	GLfloat lenQ = cast(GLfloat) Math.sqrt(
		v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
	v[0] /= lenQ;
	v[1] /= lenQ;
	v[2] /= lenQ;
	// Project v Such That We Get Two Values Along Each Texture-Coordinat Axis.
	c[0] = (s[0] * v[0] + s[1] * v[1] + s[2] * v[2]) * MAX_EMBOSS;
	c[1] = (t[0] * v[0] + t[1] * v[1] + t[2] * v[2]) * MAX_EMBOSS;
}

//MUST CALL THIS LAST!!!, Billboards The Two Logos.
void drawLogo()
{
	glDepthFunc(GL_ALWAYS);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	glEnable(GL_BLEND);
	glDisable(GL_LIGHTING);
	glLoadIdentity();
	glBindTexture(GL_TEXTURE_2D, glLogo);
	glBegin(GL_QUADS);
	glTexCoord2f(0.0f, 0.0f);
	glVertex3f(0.23f, -0.4f, -1.0f);
	glTexCoord2f(1.0f, 0.0f);
	glVertex3f(0.53f, -0.4f, -1.0f);
	glTexCoord2f(1.0f, 1.0f);
	glVertex3f(0.53f, -0.25f, -1.0f);
	glTexCoord2f(0.0f, 1.0f);
	glVertex3f(0.23f, -0.25f, -1.0f);
	glEnd();

	if (useMultitexture)
	{
		glBindTexture(GL_TEXTURE_2D, multiLogo);
		glBegin(GL_QUADS);
		glTexCoord2f(0.0f, 0.0f);
		glVertex3f(-0.53f, -0.4f, -1.0f);
		glTexCoord2f(1.0f, 0.0f);
		glVertex3f(-0.33f, -0.4f, -1.0f);
		glTexCoord2f(1.0f, 1.0f);
		glVertex3f(-0.33f, -0.3f, -1.0f);
		glTexCoord2f(0.0f, 1.0f);
		glVertex3f(-0.53f, -0.3f, -1.0f);
		glEnd();
	}
	glDepthFunc(GL_LEQUAL);
}

void doMesh1TexelUnits()
{
	GLfloat[] c = [0.0f, 0.0f, 0.0f, 1.0f]; // Holds Current Vertex
	GLfloat[] n = [0.0f, 0.0f, 0.0f, 1.0f]; // Normalized Normal Of Current Surface		
	GLfloat[] s = [0.0f, 0.0f, 0.0f, 1.0f]; // s-Texture Coordinate Direction, Normalized
	GLfloat[] t = [0.0f, 0.0f, 0.0f, 1.0f]; // t-Texture Coordinate Direction, Normalized
	GLfloat[] l = new GLfloat[4]; // Holds Our Lightposition To Be Transformed Into Object Space
	GLfloat[] minv = new GLfloat[16]; // Holds The Inverted Modelview Matrix To Do So.

	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear The Screen And The Depth Buffer

	// Build Inverse Modelview Matrix First. This Substitutes One Push/Pop With One glLoadIdentity();
	// Simply Build It By Doing All Transformations Negated And In Reverse Order.
	glLoadIdentity();
	glRotatef(-yrot, 0.0f, 1.0f, 0.0f);
	glRotatef(-xrot, 1.0f, 0.0f, 0.0f);
	glTranslatef(0.0f, 0.0f, -z);
	glGetFloatv(GL_MODELVIEW_MATRIX, minv.ptr);
	glLoadIdentity();
	glTranslatef(0.0f, 0.0f, z);

	glRotatef(xrot, 1.0f, 0.0f, 0.0f);
	glRotatef(yrot, 0.0f, 1.0f, 0.0f);

	// Transform The Lightposition Into Object Coordinates:
	l[0] = lightPosition[0];
	l[1] = lightPosition[1];
	l[2] = lightPosition[2];
	l[3] = 1.0f; // Homogenous Coordinate
	l = l.multiply(minv);

	/*	PASS#1: Use Texture "Bump"
	 No Blend
	 No Lighting
	 No Offset Texture-Coordinates */
	glBindTexture(GL_TEXTURE_2D, bump[filter]);
	glDisable(GL_BLEND);
	glDisable(GL_LIGHTING);
	drawCube();

	/* PASS#2:	Use Texture "Invbump"
	 Blend GL_ONE To GL_ONE
	 No Lighting
	 Offset Texture Coordinates 
	 */
	glBindTexture(GL_TEXTURE_2D, invbump[filter]);
	glBlendFunc(GL_ONE, GL_ONE);
	glDepthFunc(GL_LEQUAL);
	glEnable(GL_BLEND);

	glBegin(GL_QUADS);
	// Front Face	
	n[0] = 0.0f;
	n[1] = 0.0f;
	n[2] = 1.0f;
	s[0] = 1.0f;
	s[1] = 0.0f;
	s[2] = 0.0f;
	t[0] = 0.0f;
	t[1] = 1.0f;
	t[2] = 0.0f;
	for (int i = 0; i < 4; i++)
	{
		drawBump(i, c, s, n, l, t);
	}

	// Back Face	
	n[0] = 0.0f;
	n[1] = 0.0f;
	n[2] = -1.0f;
	s[0] = -1.0f;
	s[1] = 0.0f;
	s[2] = 0.0f;
	t[0] = 0.0f;
	t[1] = 1.0f;
	t[2] = 0.0f;
	for (int i = 4; i < 8; i++)
	{
		drawBump(i, c, s, n, l, t);
	}

	// Top Face	
	n[0] = 0.0f;
	n[1] = 1.0f;
	n[2] = 0.0f;
	s[0] = 1.0f;
	s[1] = 0.0f;
	s[2] = 0.0f;
	t[0] = 0.0f;
	t[1] = 0.0f;
	t[2] = -1.0f;
	for (int i = 8; i < 12; i++)
	{
		drawBump(i, c, s, n, l, t);
	}

	// Bottom Face
	n[0] = 0.0f;
	n[1] = -1.0f;
	n[2] = 0.0f;
	s[0] = -1.0f;
	s[1] = 0.0f;
	s[2] = 0.0f;
	t[0] = 0.0f;
	t[1] = 0.0f;
	t[2] = -1.0f;
	for (int i = 12; i < 16; i++)
	{
		drawBump(i, c, s, n, l, t);
	}

	// Right Face	
	n[0] = 1.0f;
	n[1] = 0.0f;
	n[2] = 0.0f;
	s[0] = 0.0f;
	s[1] = 0.0f;
	s[2] = -1.0f;
	t[0] = 0.0f;
	t[1] = 1.0f;
	t[2] = 0.0f;
	for (int i = 16; i < 20; i++)
	{
		drawBump(i, c, s, n, l, t);
	}

	// Left Face
	n[0] = -1.0f;
	n[1] = 0.0f;
	n[2] = 0.0f;
	s[0] = 0.0f;
	s[1] = 0.0f;
	s[2] = 1.0f;
	t[0] = 0.0f;
	t[1] = 1.0f;
	t[2] = 0.0f;
	for (int i = 20; i < 24; i++)
	{
		drawBump(i, c, s, n, l, t);
	}
	glEnd();

	/* PASS#3:	Use Texture "Base"
	 Blend GL_DST_COLOR To GL_SRC_COLOR (Multiplies By 2)
	 Lighting Enabled
	 No Offset Texture-Coordinates
	 */
	if (!emboss)
	{
		glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
		glBindTexture(GL_TEXTURE_2D, texture[filter]);
		glBlendFunc(GL_DST_COLOR, GL_SRC_COLOR);
		glEnable(GL_LIGHTING);
		drawCube();
	}

	xrot += xspeed;
	yrot += yspeed;

	/*	LAST PASS:	Do The Logos! */
	drawLogo();
}

void drawBump(int i, GLfloat[] c, GLfloat[] s, GLfloat[] n, GLfloat[] l,
	GLfloat[] t)
{
	c[0] = data[5 * i + 2];
	c[1] = data[5 * i + 3];
	c[2] = data[5 * i + 4];
	setUpBumps(n, c, l, s, t);
	if (maxTexelUnits > 1)
	{
		glMultiTexCoord2fARB(GL_TEXTURE0_ARB, data[5 * i], data[5 * i + 1]);
		glMultiTexCoord2fARB(GL_TEXTURE1_ARB, data[5 * i] + c[0],
			data[5 * i + 1] + c[1]);
	}
	else
	{
		glTexCoord2f(data[5 * i] + c[0], data[5 * i + 1] + c[1]);
	}
	glVertex3f(data[5 * i + 2], data[5 * i + 3], data[5 * i + 4]);
}

void doMesh2TexelUnits()
{
	GLfloat[] c = [0.0f, 0.0f, 0.0f, 1.0f]; // holds current vertex
	GLfloat[] n = [0.0f, 0.0f, 0.0f, 1.0f]; // normalized normal of current surface		
	GLfloat[] s = [0.0f, 0.0f, 0.0f, 1.0f]; // s-texture coordinate direction, normalized
	GLfloat[] t = [0.0f, 0.0f, 0.0f, 1.0f]; // t-texture coordinate direction, normalized
	GLfloat[] l = new GLfloat[4]; // holds our lightposition to be transformed into object space
	GLfloat[] minv = new GLfloat[16]; // holds the inverted modelview matrix to do so.

	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear The Screen And The Depth Buffer

	// Build Inverse Modelview Matrix First. This Substitutes One Push/Pop With One glLoadIdentity();
	// Simply Build It By Doing All Transformations Negated And In Reverse Order.
	glLoadIdentity();
	glRotatef(-yrot, 0.0f, 1.0f, 0.0f);
	glRotatef(-xrot, 1.0f, 0.0f, 0.0f);
	glTranslatef(0.0f, 0.0f, -z);
	glGetFloatv(GL_MODELVIEW_MATRIX, minv.ptr);
	glLoadIdentity();
	glTranslatef(0.0f, 0.0f, z);

	glRotatef(xrot, 1.0f, 0.0f, 0.0f);
	glRotatef(yrot, 0.0f, 1.0f, 0.0f);

	// Transform The Lightposition Into Object Coordinates:
	l[0] = lightPosition[0];
	l[1] = lightPosition[1];
	l[2] = lightPosition[2];
	l[3] = 1.0f; // Homogenous Coordinate
	l = l.multiply(minv);

	/*	PASS#1: Texel-Unit 0:	Use Texture "Bump"
	 No Blend
	 No Lighting
	 No Offset Texture-Coordinates 
	 Texture-Operation "Replace"
	 Texel-Unit 1:	Use Texture "Invbump"
	 No Lighting
	 Offset Texture Coordinates 
	 Texture-Operation "Replace"
	 */
	// TEXTURE-UNIT #0		
	glActiveTextureARB(GL_TEXTURE0_ARB);
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, bump[filter]);
	glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE_EXT);
	glTexEnvf(GL_TEXTURE_ENV, GL_COMBINE_RGB_EXT, GL_REPLACE);
	// TEXTURE-UNIT #1:
	glActiveTextureARB(GL_TEXTURE1_ARB);
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, invbump[filter]);
	glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE_EXT);
	glTexEnvf(GL_TEXTURE_ENV, GL_COMBINE_RGB_EXT, GL_ADD);
	// General Switches:
	glDisable(GL_BLEND);
	glDisable(GL_LIGHTING);
	glBegin(GL_QUADS);
	// Front Face	
	n[0] = 0.0f;
	n[1] = 0.0f;
	n[2] = 1.0f;
	s[0] = 1.0f;
	s[1] = 0.0f;
	s[2] = 0.0f;
	t[0] = 0.0f;
	t[1] = 1.0f;
	t[2] = 0.0f;
	for (int i = 0; i < 4; i++)
	{
		drawBump(i, c, s, n, l, t);
	}
	// Back Face	
	n[0] = 0.0f;
	n[1] = 0.0f;
	n[2] = -1.0f;
	s[0] = -1.0f;
	s[1] = 0.0f;
	s[2] = 0.0f;
	t[0] = 0.0f;
	t[1] = 1.0f;
	t[2] = 0.0f;
	for (int i = 4; i < 8; i++)
	{
		drawBump(i, c, s, n, l, t);
	}
	// Top Face	
	n[0] = 0.0f;
	n[1] = 1.0f;
	n[2] = 0.0f;
	s[0] = 1.0f;
	s[1] = 0.0f;
	s[2] = 0.0f;
	t[0] = 0.0f;
	t[1] = 0.0f;
	t[2] = -1.0f;
	for (int i = 8; i < 12; i++)
	{
		drawBump(i, c, s, n, l, t);
	}
	// Bottom Face
	n[0] = 0.0f;
	n[1] = -1.0f;
	n[2] = 0.0f;
	s[0] = -1.0f;
	s[1] = 0.0f;
	s[2] = 0.0f;
	t[0] = 0.0f;
	t[1] = 0.0f;
	t[2] = -1.0f;
	for (int i = 12; i < 16; i++)
	{
		drawBump(i, c, s, n, l, t);
	}
	// Right Face	
	n[0] = 1.0f;
	n[1] = 0.0f;
	n[2] = 0.0f;
	s[0] = 0.0f;
	s[1] = 0.0f;
	s[2] = -1.0f;
	t[0] = 0.0f;
	t[1] = 1.0f;
	t[2] = 0.0f;
	for (int i = 16; i < 20; i++)
	{
		drawBump(i, c, s, n, l, t);
	}
	// Left Face
	n[0] = -1.0f;
	n[1] = 0.0f;
	n[2] = 0.0f;
	s[0] = 0.0f;
	s[1] = 0.0f;
	s[2] = 1.0f;
	t[0] = 0.0f;
	t[1] = 1.0f;
	t[2] = 0.0f;
	for (int i = 20; i < 24; i++)
	{
		drawBump(i, c, s, n, l, t);
	}
	glEnd();

	/* PASS#2	Use Texture "Base"
	 Blend GL_DST_COLOR To GL_SRC_COLOR (Multiplies By 2)
	 Lighting Enabled
	 No Offset Texture-Coordinates
	 */
	glActiveTextureARB(GL_TEXTURE1_ARB);
	glDisable(GL_TEXTURE_2D);
	glActiveTextureARB(GL_TEXTURE0_ARB);
	if (!emboss)
	{
		glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
		glBindTexture(GL_TEXTURE_2D, texture[filter]);
		glBlendFunc(GL_DST_COLOR, GL_SRC_COLOR);
		glEnable(GL_BLEND);
		glEnable(GL_LIGHTING);
		drawCube();
	}

	xrot += xspeed;
	yrot += yspeed;

	/* LAST PASS:	Do The Logos! */
	drawLogo();
}

void doMeshNoBumps()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear The Screen And The Depth Buffer
	glLoadIdentity(); // Reset The View
	glTranslatef(0.0f, 0.0f, z);

	glRotatef(xrot, 1.0f, 0.0f, 0.0f);
	glRotatef(yrot, 0.0f, 1.0f, 0.0f);
	if (useMultitexture)
	{
		glActiveTextureARB(GL_TEXTURE1_ARB);
		glDisable(GL_TEXTURE_2D);
		glActiveTextureARB(GL_TEXTURE0_ARB);
	}
	glDisable(GL_BLEND);
	glBindTexture(GL_TEXTURE_2D, texture[filter]);
	glBlendFunc(GL_DST_COLOR, GL_SRC_COLOR);
	glEnable(GL_LIGHTING);
	drawCube();

	xrot += xspeed;
	yrot += yspeed;

	/* LAST PASS:	Do The Logos! */
	drawLogo();
}

/**
 * Here's Where We Do All The Drawing
 */
void drawGLScene()
{
	if (bumps)
	{
		if (useMultitexture && maxTexelUnits > 1)
		{
			doMesh2TexelUnits();
		}
		else
		{
			doMesh1TexelUnits();
		}
	}
	else
	{
		doMeshNoBumps();
	}
}

void drawCube()
{
	glBegin(GL_QUADS);
	// Front Face
	glNormal3f(0.0f, 0.0f, 1.0f);
	for (int i = 0; i < 4; i++)
	{
		drawFace(i);
	}

	// Back Face
	glNormal3f(0.0f, 0.0f, -1.0f);
	for (int i = 4; i < 8; i++)
	{
		drawFace(i);
	}

	// Top Face
	glNormal3f(0.0f, 1.0f, 0.0f);
	for (int i = 8; i < 12; i++)
	{
		drawFace(i);
	}

	// Bottom Face
	glNormal3f(0.0f, -1.0f, 0.0f);
	for (int i = 12; i < 16; i++)
	{
		drawFace(i);
	}

	// Right face
	glNormal3f(1.0f, 0.0f, 0.0f);
	for (int i = 16; i < 20; i++)
	{
		drawFace(i);
	}

	// Left Face
	glNormal3f(-1.0f, 0.0f, 0.0f);
	for (int i = 20; i < 24; i++)
	{
		drawFace(i);
	}
	glEnd();
}

void drawFace(int i)
{
	glTexCoord2f(data[5 * i], data[5 * i + 1]);
	glVertex3f(data[5 * i + 2], data[5 * i + 3], data[5 * i + 4]);
}

/**
 * Calculates v=vM, M Is 4x4 In Column-Major, v Is 4dim. Row (i.e. "Transposed")
 */
GLfloat[] multiply(GLfloat[] vec, GLfloat[] mat)
in
{
	assert (vec.length == 4);
	assert (mat.length == 16);
}
body
{
	GLfloat[] res = new GLfloat[4];
	res[0] = mat[0] * vec[0] + mat[1] * vec[1] + mat[2] * vec[2] + mat[3] * vec[3];
	res[1] = mat[4] * vec[0] + mat[5] * vec[1] + mat[6] * vec[2] + mat[7] * vec[3];
	res[2] = mat[8] * vec[0] + mat[9] * vec[1] + mat[10] * vec[2] + mat[11] * vec[3];
	res[3] = mat[15]; // Homogenous Coordinate
	return res;
}

/**
 * Initializes and opens the SDL window.
 */
void createGLWindow(char[] title, int width, int height, int bits,
	bool fullScreen)
{
	// Set the OpenGL attributes
	SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5);
	SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 6);
	SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5);
	SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
	SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

	// Set the window title
	SDL_WM_SetCaption(toStringz(title), null);

	// Note the SDL_DOUBLEBUF flag is not required to enable double
	// buffering when setting an OpenGL video mode.
	// Double buffering is enabled or disabled using the
	// SDL_GL_DOUBLEBUFFER attribute. (See above.)
	int mode = SDL_OPENGL;
	if (fullScreen)
	{
		mode |= SDL_FULLSCREEN;
	}
	// Now open a SDL OpenGL window with the given parameters
	if (SDL_SetVideoMode(width, height, bits, mode) is null)
	{
		throw new Exception("Failed to open OpenGL window: " ~ getSDLError());
	}

	resizeGLScene(width, height);
}

/**
 * Get the SDL error as a D string.
 *
 * Returns: A D string containing the current SDL error.
 */
char[] getSDLError()
{
	return fromStringz(SDL_GetError());
}
